package com.seckill.goods.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.seckill.commons.Constants;
import com.seckill.goods.mapper.GoodsMapper;
import com.seckill.goods.model.Goods;
import com.seckill.goods.service.GoodsService;
import com.seckill.orders.model.Orders;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

@Service
public class GoodsServiceImpl implements GoodsService {
    @Resource
    private GoodsMapper goodsMapper;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private AmqpTemplate amqpTemplate;
    /**
     * 获取商品列表，实际项目不能获取所有数据，应该根据系统时间获取正在进行活动以及即将开始活动的商品列表
     * 如果并发量比较高，则可以从Redis中获取数据
     * @return
     */
    public List<Goods> getGoodsAll() {
        return goodsMapper.selectAll();
    }

    /**
     * 根据商品主键获取商品详情
     * 实际项目并发来那个如果比较高，可以从Redis中获取数据
     * @param goodsId
     * @return
     */
    public Goods getGoodsById(Integer goodsId) {
        return goodsMapper.selectByPrimaryKey(goodsId);
    }

    /**
     * 秒杀的主业务逻辑方法，这来要完成减少库存 下单 防止超卖，防止重复购买，防止掉单
     * 由于并发量比较高所以不能直接操作数据库，需要利用Redis和MQ完成
     * @param goodsId 商品Id
     * @param randomName 商品随机名
     * @param price 商品价格
     * @param uid 当前登录用户的Id
     * @return 下单结果
     *  0 表示下单成功
     */
    public int secKill(Integer goodsId, String randomName, BigDecimal price, Integer uid) {
        //定义订单对象并转换成Json数据，用于存入MQ通知订单完成数据库下单，存入Redis防止掉单确保MQ的消息100%可以发送成功
        Orders orders=new Orders();
        orders.setGoodsId(goodsId);
        orders.setUid(uid);
        orders.setBuyPrice(price);
        String orderJson= JSONObject.toJSONString(orders);
        while (true) {
            /**
             * 利用匿名内部类来重写execute方法，来执行Redis的命令，返回Redis命令的执行结果
             * 返回值有2种类型
             *  1. Integer 表示出现了逻辑错误 可能是商品信息错误 没有库存或重复购买
             *  2. List  长度大于0则表示事务提交成功
             */
            Object result=stringRedisTemplate.execute(new SessionCallback<Object>() {
                @Override
                public <K, V> Object execute(RedisOperations<K, V> redisOperations) throws DataAccessException {
                    //定义List集合用于存在需要监控的Key
                    //我们需要监控商品库存防止超卖，监控用户购买记录防止重复购买
                    List key=new ArrayList();
                    key.add(Constants.GOODS_STORE+randomName);
                    key.add(Constants.BUY_RECORD+goodsId+":"+uid);
                    //这是Key监控
                    redisOperations.watch(key);
                    //获取商品库存
                    String store= (String) redisOperations.opsForValue().get(Constants.GOODS_STORE+randomName);
                    //进入if表示商品信息错误
                    if(store==null){
                        redisOperations.unwatch();
                        return 1;
                    }
                    //进入if表示没有库存
                    if(Integer.valueOf(store)<=0){
                        redisOperations.unwatch();
                        return 2;
                    }
                    //获取用户购买记录
                    String buyRecord= (String) redisOperations.opsForValue().get(Constants.BUY_RECORD+goodsId+":"+uid);
                    //进入if表示有购买记录
                    if(buyRecord!=null){
                        redisOperations.unwatch();
                        return 3;
                    }
                    //程序到了这里表示 暂时有库存 暂时没有购买记录，准备开始尝试减少库存，但是不一定能购买成功
                    //开启事务
                    redisOperations.multi();
                    //减少库存
                    redisOperations.opsForValue().decrement(((K) (Constants.GOODS_STORE + randomName)));
                    //添加购买记录
                    redisOperations.opsForValue().set(((K) (Constants.BUY_RECORD + goodsId + ":" + uid)), ((V) "1"));

                    /**
                     * 使用统一的key ，使用订单的Json作为value，使用系统时间毫秒值作为分数
                     * 将订单备份存入Redis，防止掉单
                     * 需要利用定时任务定期扫描这写数据，如果存在掉单行为则将消息补发到MQ
                     * 这个数据之后在完成数据库下单以后才会从Redis中删除掉
                     */
                    redisOperations.opsForZSet().add(((K) Constants.ORDERS), ((V) orderJson),System.currentTimeMillis());
                    //提交事务，返回List集合，List集合长度大于0则表示事务提交成功，长度等于0或为null表示事务提交失败
                    return redisOperations.exec();
                }
            });
            //进入if表示出现了逻辑错误直接返回对应的下单结果即可
            if(result instanceof Integer){
                return (int) result;
            }

            List list= (List) result;
            if(list!=null&&!list.isEmpty()){
                //结束循环
                break;
            }
        }
        //程序到了这里，表示减少了库存添加了购买记录


        //将订单消息存入MQ
        amqpTemplate.convertAndSend("secKillExchange","",orderJson);
        return 0;
    }
}
